home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2010 April / PCWorld0410.iso / pluginy Firefox / 7684 / 7684.xpi / chrome / firefm.jar / content / fmBrowserOverlay.js < prev    next >
Text File  |  2009-09-14  |  27KB  |  837 lines

  1. /**
  2.  * Copyright (c) 2008, Jose Enrique Bolanos, Jorge Villalobos
  3.  * All rights reserved.
  4.  *
  5.  * Redistribution and use in source and binary forms, with or without
  6.  * modification, are permitted provided that the following conditions are met:
  7.  *
  8.  *  * Redistributions of source code must retain the above copyright notice,
  9.  *    this list of conditions and the following disclaimer.
  10.  *  * Redistributions in binary form must reproduce the above copyright notice,
  11.  *    this list of conditions and the following disclaimer in the documentation
  12.  *    and/or other materials provided with the distribution.
  13.  *  * Neither the name of Jose Enrique Bolanos, Jorge Villalobos nor the names
  14.  *    of its contributors may be used to endorse or promote products derived
  15.  *    from this software without specific prior written permission.
  16.  *
  17.  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  18.  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  19.  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  20.  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
  21.  * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  22.  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  23.  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  24.  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  25.  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  26.  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  27.  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28.  **/
  29.  
  30. Components.utils.import("resource://firefm/fmCommon.js");
  31. Components.utils.import("resource://firefm/fmEntities.js");
  32. Components.utils.import("resource://firefm/fmPlayerInitializer.js");
  33. Components.utils.import("resource://firefm/fmPrivate.js");
  34. Components.utils.import("resource://firefm/fmNotifier.js");
  35. Components.utils.import("resource://firefm/fmFeeds.js");
  36. Components.utils.import("resource://gre/modules/Microformats.js");
  37. Components.utils.import("resource://firefm/hAudio.js");
  38.  
  39. // How much of the selected text to display (start station context menu item)
  40. const FIREFM_SELECTION_TEXT_MAX_LENGTH = 20;
  41.  
  42. // Possible values for the volume mode preference
  43. const FIREFM_PREFERENCE_VOLUME_MODE_SIMPLE = 0;
  44. const FIREFM_PREFERENCE_VOLUME_MODE_ADVANCED = 1;
  45.  
  46. /**
  47.  * FireFM chrome namespace. We need a separate one because this one is defined
  48.  * per window.
  49.  */
  50. if (typeof(FireFMChrome) == 'undefined') {
  51.   var FireFMChrome = {};
  52. };
  53.  
  54. /**
  55.  * Browser overlay controller. This is the entry point for most operations that
  56.  * happen in the toolbar.
  57.  */
  58. FireFMChrome.BrowserOverlay = {
  59.   /* The chrome URL pointing to the Fire.fm icon. */
  60.   _FIREFM_ICON_URL : "chrome://firefm/skin/logo32.png",
  61.  
  62.   /* Logger for this object. */
  63.   _logger : null,
  64.   /* String bundle in the overlay. */
  65.   _bundle : null,
  66.   /* Reference to the preferences window. */
  67.   _preferencesWindow : null,
  68.   /* The gestures preference object. */
  69.   _gesturesPref : null,
  70.   /* The amount of time within which the ban gesture should be performed. */
  71.   _banGestureTime : 0,
  72.   /* The state of a ban gesture currently in progress. */
  73.   _banGestureState : -1,
  74.   /* Ban timeout identifier. */
  75.   _banTimeout : -1,
  76.  
  77.   /**
  78.    * Initializes the object.
  79.    */
  80.   init : function() {
  81.     let that = this;
  82.  
  83.     this._logger = FireFM.getLogger("FireFMChrome.BrowserOverlay");
  84.     this._logger.debug("init");
  85.  
  86.     // get the string bundle.
  87.     this._bundle = document.getElementById("firefm-string-bundle");
  88.  
  89.     this._gesturesPref =
  90.       Application.prefs.get(FireFM.PREF_BRANCH + "useGestures");
  91.     this._banGestureTime =
  92.       Application.prefs.get(FireFM.PREF_BRANCH + "banGestureTime").value;
  93.     this._setupGestures();
  94.  
  95.     // Add listeners
  96.     document.getElementById("contentAreaContextMenu").addEventListener(
  97.       "popupshowing", FireFMChrome.BrowserOverlay.prepareContextMenu, false);
  98.     gBrowser.addEventListener(
  99.       "pageshow", function(aEvent) { that._onPageLoad(aEvent); }, true);
  100.  
  101.     if (!FireFM.startupDone) {
  102.       // XXX: this has to be run until the overlay loads because we need to
  103.       // override the behavior of several extensions.
  104.       FireFM.PlayerInitializer.init();
  105.     }
  106.  
  107.     FireFMChrome.UIState.init();
  108.     FireFMChrome.Feeds.init();
  109.     FireFM.startupDone = true;
  110.   },
  111.  
  112.   /**
  113.    * Unloads the object.
  114.    */
  115.   uninit : function() {
  116.     this._logger.debug("uninit");
  117.  
  118.     FireFMChrome.UIState.uninit();
  119.     FireFMChrome.Feeds.uninit();
  120.  
  121.     // Remove listeners
  122.     document.getElementById("contentAreaContextMenu").removeEventListener(
  123.       "popupshowing", FireFMChrome.BrowserOverlay.prepareContextMenu, false);
  124.   },
  125.  
  126.   /**
  127.    * Prepares the Fire.fm menu items located in the content area context menu
  128.    * before it is displayed.
  129.    * @param aEvent The event object associated with this event.
  130.    */
  131.   prepareContextMenu : function(aEvent) {
  132.     FireFMChrome.BrowserOverlay._logger.debug("prepareContextMenu");
  133.  
  134.     let menuArtists =
  135.       document.getElementById("firefm-context-menu-artists");
  136.     let menuStartStation =
  137.       document.getElementById("firefm-context-menu-start-station");
  138.     let selection =
  139.       document.commandDispatcher.focusedWindow.getSelection().toString();
  140.  
  141.     if (0 == selection.length) {
  142.       menuArtists.hidden = false;
  143.       menuStartStation.hidden = true;
  144.     } else {
  145.       menuStartStation.setAttribute("selection", selection);
  146.  
  147.       // Crop selection
  148.       if (FIREFM_SELECTION_TEXT_MAX_LENGTH < selection.length) {
  149.         selection =
  150.           selection.substring(0, FIREFM_SELECTION_TEXT_MAX_LENGTH) + "...";
  151.       }
  152.  
  153.       menuStartStation.setAttribute("label",
  154.         FireFMChrome.BrowserOverlay._bundle.getFormattedString(
  155.           "firefm.context.play.label", [selection]));
  156.  
  157.       menuArtists.hidden = true;
  158.       menuStartStation.hidden = false;
  159.     }
  160.   },
  161.  
  162.   /**
  163.    * Opens the Last.fm home page.
  164.    * @param aEvent the event that triggered this action.
  165.    */
  166.   openHome : function(aEvent) {
  167.     this._logger.debug("openHome");
  168.     window.openUILink(FireFM.Remote.URL_HOME, aEvent);
  169.   },
  170.  
  171.   /**
  172.    * Opens the login or logout page for Last.fm, depending the user's current
  173.    * state.
  174.    * @param aEvent the event that triggered this action.
  175.    */
  176.   loginLogout : function(aEvent) {
  177.     this._logger.debug("loginLogout");
  178.  
  179.     let url;
  180.  
  181.     if (aEvent.target.hasAttribute("loggedin")) {
  182.       url = FireFM.Login.URL_LOGOUT;
  183.     } else {
  184.       url = FireFM.Login.URL_LOGIN;
  185.     }
  186.  
  187.     window.openUILink(url, aEvent);
  188.   },
  189.  
  190.   /**
  191.    * Displays a dialog that asks the user to enter an artist, tag or user name
  192.    * to start a station. The input is then verified against Last.fm.
  193.    * @param aEvent the event that triggered this action.
  194.    */
  195.   startStation : function(aEvent) {
  196.     this._logger.debug("startStation");
  197.  
  198.     if (window.navigator.onLine &&
  199.         (FireFM.Player.STATUS_READY == FireFM.Player.status)) {
  200.       let station = { type : -1, value : "" };
  201.  
  202.       window.openDialog(
  203.         "chrome://firefm/content/fmStartStationDialog.xul",
  204.         "firefm-start-station-dialog",
  205.         "chrome,modal,centerscreen,titlebar,toolbar,resizable=no", station);
  206.  
  207.       if ((-1 != station.type) && (0 < station.value.length)) {
  208.         this.verifyStation(station.value, station.type);
  209.       }
  210.     } else {
  211.       let promptService =
  212.         Cc["@mozilla.org/embedcomp/prompt-service;1"].
  213.           getService(Ci.nsIPromptService);
  214.  
  215.       promptService.alert(
  216.         window,
  217.         FireFM.overlayBundle.GetStringFromName("firefm.startAStation.label"),
  218.         FireFM.overlayBundle.GetStringFromName("firefm.offline.label"));
  219.     }
  220.   },
  221.  
  222.   /**
  223.    * Verifies the station information entered by the user against Last.fm, and
  224.    * loads the station if it's correct. Otherwise it opens a search page with
  225.    * the given search string.
  226.    * @param aId the ID of the station.
  227.    * @param aType the type of the station.
  228.    */
  229.   verifyStation : function(aId, aType) {
  230.     this._logger.debug("verifyStation");
  231.  
  232.     let that = this;
  233.     let id = unescape(aId);
  234.  
  235.     FireFM.Station.verifyStation(
  236.       id, aType, function(aResult) { that._verifyStationLoad(aResult, aType) });
  237.   },
  238.  
  239.   /**
  240.    * Load handler for the verify station request.
  241.    * @param aResult the resulting object from the request. See
  242.    * FireFM.Station.verifyStation for more information.
  243.    * @param aType the type of station to load.
  244.    */
  245.   _verifyStationLoad : function(aResult, aType) {
  246.     this._logger.debug("_verifyStationLoad");
  247.  
  248.     if (aResult.success) {
  249.       FireFM.Station.setStation(aResult.result, aType);
  250.       FireFM.Station.play();
  251.       this._logger.debug("_verifyStationLoad. Player loaded");
  252.     } else {
  253.       window.openUILinkIn(aResult.result, "tab");
  254.       FireFM.obsService.notifyObservers(
  255.         null, FireFM.Station.TOPIC_STATION_ERROR,
  256.         FireFM.Station.ERROR_NOT_FOUND);
  257.     }
  258.   },
  259.  
  260.   /**
  261.    * Starts a station with the recommendations from Last.fm for the logged in
  262.    * user.
  263.    * @param aEvent the event that triggered this action.
  264.    */
  265.   startRecommendations : function(aEvent) {
  266.     this._logger.debug("startRecommendations");
  267.     FireFM.Station.setStation(
  268.       FireFM.Login.userName, FireFM.Station.TYPE_RECOMMENDED);
  269.     FireFM.Station.play();
  270.   },
  271.  
  272.   /**
  273.    * Starts a station with the current user's library.
  274.    * @param aEvent the event that triggered this action.
  275.    */
  276.   startMyLibrary : function(aEvent) {
  277.     this._logger.debug("startMyLibrary");
  278.  
  279.     FireFM.Station.setStation(FireFM.Login.userName, FireFM.Station.TYPE_USER);
  280.     FireFM.Station.play();
  281.   },
  282.  
  283.   /**
  284.    * Starts a station based on the user's 'neighbors'.
  285.    * @param aEvent the event that triggered this action.
  286.    */
  287.   startNeighborhood : function(aEvent) {
  288.     this._logger.debug("startNeighborhood");
  289.  
  290.     FireFM.Station.setStation(
  291.       FireFM.Login.userName, FireFM.Station.TYPE_NEIGHBORHOOD);
  292.     FireFM.Station.play();
  293.   },
  294.  
  295.   /**
  296.    * Starts a station with the current user's loved tracks.
  297.    * @param aEvent the event that triggered this action.
  298.    */
  299.   startLovedTracks : function(aEvent) {
  300.     this._logger.debug("startLovedTracks");
  301.  
  302.     FireFM.Station.setStation(FireFM.Login.userName, FireFM.Station.TYPE_LOVED);
  303.     FireFM.Station.play();
  304.   },
  305.  
  306.   /**
  307.    * Starts the station for a friend or neighbor.
  308.    * @param aEvent the event that triggered this action.
  309.    */
  310.   startUserStation : function(aEvent) {
  311.     this._logger.debug("startUserStation");
  312.  
  313.     let userName = aEvent.originalTarget.getAttribute("label");
  314.  
  315.     FireFM.Station.setStation(userName, FireFM.Station.TYPE_USER);
  316.     FireFM.Station.play();
  317.   },
  318.  
  319.   /**
  320.    * Starts the station for a top artist.
  321.    * @param aEvent the event that triggered this action.
  322.    */
  323.   startArtistStation : function(aEvent) {
  324.     this._logger.debug("startArtistStation");
  325.  
  326.     let artist = aEvent.originalTarget.getAttribute("label");
  327.  
  328.     this.verifyStation(artist, FireFM.Station.TYPE_ARTIST);
  329.   },
  330.  
  331.   /**
  332.    * Starts a recently played station.
  333.    * @param aEvent the event that triggered this action.
  334.    */
  335.   startRecentStation : function(aEvent) {
  336.     this._logger.debug("startRecentStation");
  337.  
  338.     let menuItem = aEvent.originalTarget;
  339.     let stationId = menuItem.getAttribute("fmstationid");
  340.     let stationType = parseInt(menuItem.getAttribute("fmstationtype"), 10);
  341.  
  342.     FireFM.Station.setStation(stationId, stationType);
  343.     FireFM.Station.play();
  344.   },
  345.  
  346.   /**
  347.    * Plays or stops playback on a station.
  348.    * @param aEvent the event that triggered this action.
  349.    */
  350.   playStop : function(aEvent) {
  351.     this._logger.debug("playStop");
  352.  
  353.     if (aEvent.target.hasAttribute("playing")) {
  354.       FireFM.Station.stop();
  355.     } else {
  356.       FireFM.Station.play();
  357.     }
  358.   },
  359.  
  360.   /**
  361.    * Skips to the next track in the playlist.
  362.    * @param aEvent the event that triggered this action.
  363.    */
  364.   skip : function(aEvent) {
  365.     this._logger.debug("skip");
  366.     FireFM.Station.skip();
  367.   },
  368.  
  369.   /**
  370.    * Handles the oncommand event of the volume control toolbar and status bar
  371.    * button. Depending on the volume mode preference, it shows the volume slider
  372.    * or toggles the volume mute.
  373.    * @param aEvent The event that triggered this action.
  374.    */
  375.   onVolumeCommand : function(aEvent) {
  376.     this._logger.debug("onVolumeCommand");
  377.  
  378.     let volumeModePref =
  379.       FireFM.Application.prefs.get(FireFM.PREF_BRANCH + "volume.mode");
  380.  
  381.     if (volumeModePref.value == FIREFM_PREFERENCE_VOLUME_MODE_ADVANCED) {
  382.       this.toggleMute();
  383.     } else {
  384.       aEvent.target.firstChild.openPopup(aEvent.target, 'after_start');
  385.     }
  386.   },
  387.  
  388.   /**
  389.    * Sets the volume of the player.
  390.    * @param aLevel The volume level to set.
  391.    * @param aPersist True if the volume preference must be set. False otherwise.
  392.    */
  393.   setVolume : function(aLevel, aPersist) {
  394.     // XXX: there is no logging here for performance reasons.
  395.     try {
  396.       FireFM.Player.setVolume(aLevel);
  397.     } catch(e) {
  398.       this._logger.error("setVolume. Error:\n" + e);
  399.     }
  400.  
  401.     if (aPersist) {
  402.       FireFMChrome.UIState.volumePref.value = aLevel;
  403.     }
  404.   },
  405.  
  406.   /**
  407.    * Toggles the volume level between the minimum and maximum levels.
  408.    */
  409.   toggleMute : function() {
  410.     this._logger.debug("toggleVolume");
  411.  
  412.     try {
  413.       if (FireFM.Player.volume == 0) {
  414.         this.setVolume(100, true);
  415.       } else {
  416.         this.setVolume(0, true);
  417.       }
  418.     } catch (e) {
  419.       this._logger.error("toggleVolume. Error:\n" + e);
  420.     }
  421.   },
  422.  
  423.   /**
  424.    * Checks the appropriate volume menu item depending on the current volume
  425.    * level.
  426.    */
  427.   checkVolumeItem : function(aEvent) {
  428.     this._logger.debug("checkVolumeItem");
  429.  
  430.     let level = FireFMChrome.UIState.volumePref.value;
  431.     // calculate which child should be checked.
  432.     let index = 10 - (Math.round(level / 10));
  433.  
  434.     aEvent.target.childNodes[index].setAttribute("checked", true);
  435.   },
  436.  
  437.   /**
  438.    * Opens the share track dialog.
  439.    * @param aEvent The event that triggered this action.
  440.    */
  441.   /*shareTrack : function(aEvent) {
  442.     this._logger.debug("shareTrack");
  443.  
  444.     alert("open share dialog");
  445.   },*/
  446.  
  447.   /**
  448.    * Opens the tag track dialog.
  449.    * @param aEvent The event that triggered this action.
  450.    */
  451.   tagTrack : function(aEvent) {
  452.     this._logger.debug("tagTrack");
  453.  
  454.     let currentTrack = FireFM.Playlist.currentTrack;
  455.  
  456.     if (null != currentTrack) {
  457.  
  458.       let tags = { newTags : [], removedTags : [], track : null, type : -1 };
  459.       tags.track = currentTrack;
  460.  
  461.       window.openDialog(
  462.         "chrome://firefm/content/fmTagDialog.xul",
  463.         "firefm-tag-dialog",
  464.         "chrome,modal,centerscreen,titlebar,toolbar,resizable=no", tags);
  465.  
  466.       if (-1 != tags.type) {
  467.         if (0 < tags.newTags.length) {
  468.           FireFM.Remote.addTags(tags.track, tags.type, tags.newTags);
  469.         }
  470.         for (let i = 0; i < tags.removedTags.length; i++) {
  471.           FireFM.Remote.removeTag(tags.track, tags.type, tags.removedTags[i]);
  472.         }
  473.       }
  474.     }
  475.   },
  476.  
  477.   /**
  478.    * Marks the currently played track as 'loved'.
  479.    * @param aEvent The event that triggered this action.
  480.    */
  481.   loveTrack : function(aEvent) {
  482.     this._logger.debug("loveTrack");
  483.  
  484.     if (null != FireFM.Playlist.currentTrack) {
  485.       // this is done to immediately disable the button and prevent multiple
  486.       // submissions.
  487.       FireFM.obsService.notifyObservers(
  488.         null, FireFM.Remote.TOPIC_TRACK_LOVED, null);
  489.       FireFM.Remote.loveTrack();
  490.     }
  491.   },
  492.  
  493.   /**
  494.    * Marks the currently played track as 'banned' and skips to the next one.
  495.    * @param aEvent The event that triggered this action.
  496.    */
  497.   banTrack : function(aEvent) {
  498.     this._logger.debug("banTrack");
  499.  
  500.     if (null != FireFM.Playlist.currentTrack) {
  501.       FireFM.Remote.banTrack();
  502.       this.skip(aEvent);
  503.     }
  504.   },
  505.  
  506.   /**
  507.    * Opens the track video dialog.
  508.    * @param aEvent The event that triggered this action.
  509.    */
  510.   openTrackVideo : function(aEvent) {
  511.     this._logger.debug("openTrackVideo");
  512.  
  513.     let currentTrack = FireFM.Playlist.currentTrack;
  514.  
  515.     if (null != currentTrack && null != currentTrack.video) {
  516.  
  517.       let args = { track : null };
  518.       args.track = currentTrack;
  519.  
  520.       window.openDialog(
  521.         "chrome://firefm/content/fmVideoDialog.xul",
  522.         "firefm-video-dialog",
  523.         "chrome,modal,centerscreen,titlebar,toolbar,resizable=no", args);
  524.     }
  525.   },
  526.  
  527.   /**
  528.    * Fills the recent station menu popup with the most recently played stations.
  529.    * @param aEvent the event that triggered this action.
  530.    */
  531.   fillRecentStations : function(aEvent) {
  532.     this._logger.debug("fillRecentStations");
  533.  
  534.     let popup = aEvent.target;
  535.     let recent = FireFM.History.stationHistory;
  536.     let recentCount = recent.length;
  537.     let menuItem;
  538.     let station;
  539.  
  540.     // clear the popup.
  541.     while (null != popup.firstChild) {
  542.       popup.removeChild(popup.firstChild);
  543.     }
  544.  
  545.     if (0 < recentCount) {
  546.       for (let i = 0; i < recentCount; i++) {
  547.         station = recent[i];
  548.         menuItem = document.createElement("menuitem");
  549.         menuItem.setAttribute("label",  station.title);
  550.         menuItem.setAttribute("fmstationid",  station.id);
  551.         menuItem.setAttribute("fmstationtype", station.type);
  552.         popup.appendChild(menuItem);
  553.       }
  554.     } else {
  555.       menuItem = document.createElement("menuitem");
  556.       menuItem.setAttribute(
  557.         "label",  this._bundle.getString("firefm.emptyMenu.label"));
  558.       menuItem.setAttribute("disabled", true);
  559.       popup.appendChild(menuItem);
  560.     }
  561.   },
  562.  
  563.   /**
  564.    * Opens a page related to the currently playing track.
  565.    * @param aEvent the event that triggered this action.
  566.    */
  567.   openTrackPage : function(aEvent) {
  568.     this._logger.debug("openTrackPage");
  569.     window.openUILink(aEvent.target.getAttribute("fmurl"), aEvent);
  570.   },
  571.  
  572.   /**
  573.    * Opens the preferences window.
  574.    * @param aPaneId (optional) the id of the pane to open on the window.
  575.    */
  576.   openPreferences : function(aPaneId) {
  577.     this._logger.debug("openPreferences");
  578.  
  579.     if (null == this._preferencesWindow || this._preferencesWindow.closed) {
  580.       let instantApply =
  581.         FireFM.Application.prefs.get("browser.preferences.instantApply");
  582.       let features =
  583.         "chrome,titlebar,toolbar,centerscreen" +
  584.         (instantApply.value ? ",dialog=no" : ",modal");
  585.  
  586.       this._preferencesWindow =
  587.         window.openDialog(
  588.           "chrome://firefm/content/fmPreferencesWindow.xul",
  589.           "firefm-preferences-window", features, aPaneId);
  590.     }
  591.  
  592.     this._preferencesWindow.focus();
  593.   },
  594.  
  595.   /**
  596.    * Loads the hAudio microformats found in the current document into the given
  597.    * menu popup.
  598.    * @param aMenuPopup The menu popup to be loaded with the hAudio microformats.
  599.    */
  600.   loadAudioMicroformats : function(aMenuPopup) {
  601.     this._logger.debug("loadAudioMicroformats");
  602.  
  603.     let audios = Microformats.get("hAudio", content.document);
  604.  
  605.     while (aMenuPopup.firstChild) {
  606.       aMenuPopup.removeChild(aMenuPopup.firstChild);
  607.     }
  608.  
  609.     if (0 < audios.length) {
  610.       let artists = new Array();
  611.       let startCommand =
  612.         "FireFMChrome.BrowserOverlay.verifyStation(" +
  613.         "this.getAttribute('label'), FireFM.Station.TYPE_ARTIST)";
  614.       let artist = null;
  615.       let item;
  616.  
  617.       for (var i = 0; i < audios.length; i++) {
  618.         if (typeof(audios[i].contributor) != 'undefined') {
  619.           artists.push(audios[i].contributor);
  620.         }
  621.       }
  622.  
  623.       artists.sort();
  624.  
  625.       for (var i = 0; i < artists.length; i++) {
  626.         if (artists[i] != artist) {
  627.           artist = artists[i];
  628.  
  629.           item = document.createElement("menuitem");
  630.           item.setAttribute("label", artist);
  631.           item.setAttribute("oncommand", startCommand);
  632.           aMenuPopup.appendChild(item);
  633.         }
  634.       }
  635.     }
  636.  
  637.     if (!aMenuPopup.hasChildNodes()) {
  638.       item = document.createElement("menuitem");
  639.       item.setAttribute(
  640.         "label", this._bundle.getString("firefm.emptyMenu.label"));
  641.       item.setAttribute("disabled", true);
  642.       aMenuPopup.appendChild(item);
  643.     }
  644.   },
  645.  
  646.   /**
  647.    * Displays a notification to the user informing that she needs to approve
  648.    * Fire.fm to have access to the Last.fm API.
  649.    */
  650.   showAPINotification : function() {
  651.     this._logger.debug("showAPINotification");
  652.  
  653.     let nb = gBrowser.getNotificationBox();
  654.     let permissionButton = new Object();
  655.     let that = this;
  656.     let message;
  657.  
  658.     permissionButton.label =
  659.       FireFM.overlayBundle.GetStringFromName("firefm.givePermission.label");
  660.     permissionButton.accessKey =
  661.       FireFM.overlayBundle.GetStringFromName("firefm.givePermission.accesskey");
  662.     permissionButton.popup = null;
  663.     permissionButton.callback =
  664.       function(aNotification) { that._openAPIPermissionTab(aNotification); };
  665.  
  666.     message =
  667.       FireFM.overlayBundle.GetStringFromName(
  668.         "firefm.apiNotification.label");
  669.  
  670.     nb.appendNotification(
  671.       message, "firefm-api-notification", this._FIREFM_ICON_URL,
  672.       nb.PRIORITY_INFO_HIGH, [ permissionButton ] );
  673.   },
  674.  
  675.   /**
  676.    * Opens the API permission URL in a new tab, and adds an event listener that
  677.    * allows us to know the value of the token returned by the API.
  678.    * @param aNotification the notification box element that called this method.
  679.    */
  680.   _openAPIPermissionTab : function(aNotification) {
  681.     this._logger.trace("_openAPIPermissionTab");
  682.     gBrowser.selectedTab = gBrowser.addTab(FireFM.Login.URL_API_ACCESS);
  683.     aNotification.close();
  684.   },
  685.  
  686.   /**
  687.    * Handles the page load event for all pages loaded in the window. This is
  688.    * done to handle the loading of the API permission page.
  689.    * @param aEvent the event that triggered this action.
  690.    */
  691.   _onPageLoad : function(aEvent) {
  692.     // XXX: no logging here for performance purposes.
  693.     let doc = aEvent.originalTarget;
  694.  
  695.     if (FireFM.Login.URL_API_ACCESS_RE.test(doc.documentURI)) {
  696.       let browser = gBrowser.getBrowserForDocument(doc);
  697.  
  698.       // prevent adding more than one listener.
  699.       if (!browser.hasAttribute("firefmpermission")) {
  700.         // add a progress listener so that we can detect when to pick up the API
  701.         // token.
  702.         browser.webProgress.addProgressListener(
  703.           FireFMChrome.ProgressListener, Ci.nsIWebProgress.NOTIFY_STATE_ALL);
  704.         browser.setAttribute("firefmpermission", true);
  705.       }
  706.     }
  707.   },
  708.  
  709.   /**
  710.    * Sets up advanced mouse gestures by replacing a function in browser.js.
  711.    * XXX: the code in browser.js makes it very difficult for extensions to
  712.    * resgister listeners to the gesture events, so we need to do this hack to
  713.    * support them. See:
  714.    * http://mxr.mozilla.org/mozilla1.9.1/source/browser/base/content/
  715.    * browser.js#713
  716.    * @param aEvent the event that triggered this action.
  717.    */
  718.   _setupGestures : function() {
  719.     this._logger.trace("_setupGestures");
  720.  
  721.     if (("undefined" != typeof(gGestureSupport)) && gGestureSupport.onSwipe) {
  722.       this._logger.debug("_setupGestures. Gestures supported.");
  723.  
  724.       let that = this;
  725.       let oldOnSwipe = gGestureSupport.onSwipe;
  726.  
  727.       gGestureSupport.onSwipe = function(aEvent) {
  728.         if (that._gesturesPref.value) {
  729.           switch (aEvent.direction) {
  730.             case aEvent.DIRECTION_UP:
  731.               let more = (FireFMChrome.UIState.volumePref.value + 25);
  732.  
  733.               FireFMChrome.BrowserOverlay.setVolume(
  734.                 ((100 >= more) ? more : 100), true);
  735.               break;
  736.             case aEvent.DIRECTION_DOWN:
  737.               let less = (FireFMChrome.UIState.volumePref.value - 25);
  738.  
  739.               FireFMChrome.BrowserOverlay.setVolume(
  740.                 ((0 <= less) ? less : 0), true);
  741.               break;
  742.             case aEvent.DIRECTION_RIGHT:
  743.               if (1 == that._banGestureState) {
  744.                 // swipe right during ban operation.
  745.                 that._banGestureState = 2;
  746.               } else if (FireFM.Player.isPlaying) {
  747.                 FireFM.Station.skip();
  748.               }
  749.               break;
  750.             case aEvent.DIRECTION_LEFT:
  751.               if (2 == that._banGestureState) {
  752.                 // second left swipe, perform the ban operation.
  753.                 that.banTrack(aEvent);
  754.                 window.clearTimeout(that._banTimeout);
  755.                 that._banGestureState = -1;
  756.                 that._banTimeout = -1;
  757.               } else {
  758.                 // first left swipe. Set state and timer.
  759.                 that._banGestureState = 1;
  760.  
  761.                 if (that._banTimeout) {
  762.                   window.clearTimeout(that._banTimeout);
  763.                 }
  764.                 // the timeout clears the state if the gesture takes too long,
  765.                 // given that it's likely the user wasn't meaning to make such a
  766.                 // slow gesture.
  767.                 that._banTimeout =
  768.                   window.setTimeout(
  769.                     function() {
  770.                       that._banGestureState = -1;
  771.                       that._banTimeout = -1;
  772.                     },
  773.                     that._banGestureTime);
  774.               }
  775.               break;
  776.           }
  777.         } else {
  778.           oldOnSwipe.call(gGestureSupport, aEvent);
  779.         }
  780.       }
  781.     }
  782.   }
  783. };
  784.  
  785. /**
  786.  * Progress listener object used to detect important page loads. Implements
  787.  * nsIWebProgressListener, but doesn't implement most methods.
  788.  */
  789. FireFMChrome.ProgressListener = {
  790.   /* Regular expression used to extract the token from the callback URL. */
  791.   _RE_GET_TOKEN :
  792.     /^http\:\/\/(?:[a-z]+\.)?last(?:\.)?fm(?:[a-z\.]+)?\/\?token\=([a-z0-9]+)$/i,
  793.  
  794.   onStateChange : function(aWebProgress, aRequest, aStateFlags, aStatus) {
  795.     // XXX: there is no logging here for performance reasons.
  796.     if ((aStatus & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT) &&
  797.         (aStatus & Ci.nsIWebProgressListener.STATE_START)) {
  798.       FireFMChrome.BrowserOverlay._logger.trace(
  799.         "onStateChange. Document start.");
  800.  
  801.       let match = this._RE_GET_TOKEN.exec(aRequest.name);
  802.  
  803.       if ((null != match) && (2 == match.length)) {
  804.         FireFM.Remote.authGetSession(match[1]);
  805.       }
  806.     }
  807.   },
  808.  
  809.   onProgressChange : function(
  810.     aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress,
  811.     aCurTotalProgress, aMaxTotalProgress) {},
  812.   onLocationChange : function(aWebProgress, aRequest, aLocation) {},
  813.   onStatusChange : function(aWebProgress, aRequest, aStatus, aMessage) {},
  814.   onSecurityChange : function(aWebProgress, aRequest, aState) {},
  815.  
  816.   /**
  817.    * The QueryInterface method provides runtime type discovery.
  818.    * More: http://developer.mozilla.org/en/docs/nsISupports
  819.    * @param aIID the IID of the requested interface.
  820.    * @return the resulting interface pointer.
  821.    */
  822.   QueryInterface : function(aIID) {
  823.     if (!aIID.equals(Ci.nsIWebProgressListener) &&
  824.         !aIID.equals(Ci.nsISupportsWeakReference) &&
  825.         !aIID.equals(Ci.nsISupports)) {
  826.       throw Components.results.NS_ERROR_NO_INTERFACE;
  827.     }
  828.  
  829.     return this;
  830.   }
  831. };
  832.  
  833. window.addEventListener(
  834.   "load", function() { FireFMChrome.BrowserOverlay.init(); }, false);
  835. window.addEventListener(
  836.   "unload", function() { FireFMChrome.BrowserOverlay.uninit(); }, false);
  837.